/*
 * TowerBase.java
 *
 * Created on November 24, 2007, 3:19 PM
 *
 * Copyright (C) 2007-2008  Hylke van der Schaaf
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, in version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package hextd.tower;

import hextd.hex.Hex;
import hextd.sprite.EnemySprite;
import hextd.util.Context;
import hextd.util.TowerChangeListener;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.text.DecimalFormat;
import java.util.Vector;

/**
 *
 * @author hylke
 */
public abstract class TowerBase implements Tower {
    
    protected float paintScale;
    protected Context context;
    
    private TowerFactory.type type;
    private TowerFireControl fc;
    protected Vector<TowerUpgradeDamage> upgradeDamage;
    protected Vector<TowerUpgradeRange>  upgradeRange;
    private Vector<TowerChangeListener>  towerChangeListeners;
    protected int mode = 0;
    protected String[] modes = {"Close", "Hard", "Weak", "Furthest"};
    protected boolean retarget = false;
    
    protected Hex hex;
    protected int[] hexLocation;
    protected float xFloat,yFloat;
    protected float xPaint,yPaint;
    
    protected int level = 0;
    protected float rangeBase = 0;
    protected float rangeCurrent = 0;
    protected float rangeCurrent2 = 0;
    protected float rangePaint = 0;
    protected int damageBase = 0;
    protected int damageCurrent = 0;
    protected String damageText = "";
    protected String rangeText = "";
    
    
    protected static DecimalFormat form = new DecimalFormat("#,###.##");
    protected PanelTowerSettings pts;
    
    protected Color     lineColor;
    protected Color     rangeColor;
    protected Shape[]   shapes;
    protected int[]     shapeAction;
    protected Color[]   shapeColor;
    protected Stroke[]  strokes;
    protected Shape     rangeCircle;
    protected Stroke    rangeStroke;
    protected Font      levelFont;
    protected float[]   levelLocation;
    protected AffineTransform atTranslate = new AffineTransform();
    private AffineTransform atCorrect;
    
    private boolean bodyUsesSVG;
    private String  bodySVGName;
    private float   bodyScale;
    
    protected boolean selected = false;
    protected boolean selling = false;
    private int price = 0;
    /**
     * Creates a new instance of TowerBase
     */
    public TowerBase(TowerFactory.type t, int price, int damage, float range) {
        this.price = price;
        this.type = t;
        this.damageBase = damage;
        this.rangeBase = range;
        this.levelLocation = new float[2];
        this.upgradeDamage = new Vector<TowerUpgradeDamage>();
        this.upgradeRange = new Vector<TowerUpgradeRange>();
        this.towerChangeListeners = new Vector<TowerChangeListener>();
    }
    
    protected void doInit(Context context, Hex hex) {
        this.context = context;
        this.rangeColor = this.context.colors.getColorRangeCircle();
        this.damageCurrent = this.damageBase;
        
        this.hex = hex;
        if (hex != null) {
            this.hexLocation = hex.getLocation();
        }
        
        this.upgradeDamage = new Vector<TowerUpgradeDamage>();
        this.upgradeRange = new Vector<TowerUpgradeRange>();
        PanelTowerSettings p = new PanelTowerSettings(this, this.context);
        p.setModes(this.modes);
        this.pts = p;
        this.atCorrect = new AffineTransform();
        this.resetScale();
    }
    
    @Override
    public boolean setMode(int m, int v) {
        if (m==1) {
            this.mode = v;
            if (this.mode >= this.modes.length) {
                this.mode = 0;
            }
            this.retarget = true;
            if (this.pts.getMode(m) != this.mode) {
                this.pts.setMode(m, this.mode);
            }
            return true;
        } else {
            System.err.println("TowerBase::setMode: unknown mode: "+m);
            return false;
        }
    }
    @Override
    public int nextMode(int m) {
        return this.pts.nextMode(m);
    }
    @Override
    public int getMode(int m) {
        if (m==1) {
            return this.mode;
        } else {
            System.err.println("TowerBase::setMode: unknown mode: "+m);
            return -1;
        }
    }
    
    
    @Override
    public float getRangeBase() {
        return this.rangeBase;
    }
    
    @Override
    public float getRangeCurrent() {
        return this.rangeCurrent;
    }
    
    @Override
    public int getUpgradePrice() {
        return Math.round(0.5f * this.price);
    }
    
    @Override
    public int getSellPrice() {
        return Math.round(0.75f * (1.0f + 0.5f * this.level) * this.price);
    }
    
    @Override
    public boolean canUpgrade() {
        return (this.level<9);
    }
    
    @Override
    public void doUpgrade() {
        this.level++;
        this.damageCurrent = (int)(this.damageBase * (float)Math.pow(1.45, this.level));
        this.calcDamageRange();
        this.fireTowerChangedEvent();
    }
    protected void calcDamageRange() {
        float lvl = (float)Math.pow(1.45, this.level);
        float upg = (1f + 0.25f*this.upgradeDamage.size());
        this.damageCurrent = (int)(this.damageBase * lvl * upg );
        this.damageText = ""+TowerBase.form.format(this.damageBase)+"*"+TowerBase.form.format(lvl)+"*"+TowerBase.form.format(upg)+"="+TowerBase.form.format(this.damageCurrent);
        
        lvl = (float)Math.pow(1.07, this.level);
        upg = (1f + 0.25f*this.upgradeRange.size());
        this.rangeCurrent = this.rangeBase * lvl * upg;
        this.rangeText = ""+TowerBase.form.format(this.rangeBase)+"*"+TowerBase.form.format(lvl)+"*"+TowerBase.form.format(upg)+"="+TowerBase.form.format(this.rangeCurrent);
        
        this.rangePaint = this.rangeCurrent * this.paintScale;
        this.rangeCurrent2 = this.rangeCurrent*this.rangeCurrent;
        this.rangeCircle = new Ellipse2D.Float(-rangePaint, -rangePaint, rangePaint*2, rangePaint*2);
    }
    
    abstract void generateShapes();
    public String getSvgName() {
        return null;
    }
    
    @Override
    public void setSelected(boolean selected) {
        this.selected = selected;
    }
    
    /* Non specific enemy finders
     * There are used for bouncing projectiles and splash damage
     * These can not be deferred to a firecontrol tower.
     */
    
    protected EnemySprite[] findEnemiesInRange(float x, float y, float r) {
        Vector<EnemySprite> tempEnemies = new Vector<EnemySprite>();
        float r2 = r*r;
        float dx,dy, d2;
        if (this.context.enemies != null) {
            for (int i=0; i<this.context.enemies.length; i++) {
                if (this.context.enemies[i].validTarget()) {
                    dx = this.context.enemies[i].getX() - x;
                    dy = this.context.enemies[i].getY() - y;
                    d2 = dx*dx + dy*dy;
                    if (d2 < r2) {
                        tempEnemies.add(this.context.enemies[i]);
                    }
                }
            }
        }
        EnemySprite[] retVal = new EnemySprite[tempEnemies.size()];
        retVal = tempEnemies.toArray(retVal);
        return retVal;
    }
    
    protected int findEnemyClosest(float x, float y, float r, int[] not) {
        
        int foundEnemy = -1;
        float distance2Min = r*r;
        float distance2;
        float dx,dy;
        boolean found;
        if (this.context.enemies != null) {
            for (int i=0; i<this.context.enemies.length; i++) {
                found = false;
                for (int n=0; n<not.length; n++) {
                    if (not[n] == i) {
                        found = true;
                        continue;
                    }
                }
                if (!found && this.context.enemies[i].validTarget()) {
                    dx = this.context.enemies[i].getX()-x;
                    dy = this.context.enemies[i].getY()-y;
                    distance2 = dx*dx + dy*dy;
                    if (distance2 < distance2Min) {
                        distance2Min = distance2;
                        foundEnemy = i;
                    }
                }
            }
        }
        return foundEnemy;
    }
    
    /* Specific enemy finders
     * These can not be deferred to a firecontrol tower.
     */
    
    protected EnemySprite[] findEnemiesFastest(float x, float y, float r, int n, boolean exShielded) {
        return this.findEnemiesFastest(x, y, r, n, new EnemySprite[0], exShielded);
    }
    
    protected EnemySprite[] findEnemiesFastest(float x, float y, float r, EnemySprite[] preferred, int n, boolean exShielded) {
        Vector<EnemySprite> found = new Vector<EnemySprite>(n);
        float r2 = r*r;
        float dx,dy, d2;
        EnemySprite pe;
        for (int p=0; p<preferred.length; p++) {
            pe = preferred[p];
            if (pe.validTarget()) {
                dx = pe.getX() - x;
                dy = pe.getY() - y;
                d2 = dx*dx + dy*dy;
                if (d2 < r2) {
                    found.add(pe);
                }
            }
        }
        EnemySprite[] not = new EnemySprite[found.size()];
        not = found.toArray(not);
        EnemySprite[] extra = this.findEnemiesFastest(x, y, r, n-not.length, not, exShielded);
        for (int e=0; e<extra.length; e++) {
            found.add(extra[e]);
        }
        EnemySprite[] retval = new EnemySprite[found.size()];
        return found.toArray(retval);
    }
    
    protected EnemySprite[] findEnemiesFastest(float x, float y, float r, int n, EnemySprite[] not, boolean exShielded) {
        
        Vector<EnemySprite> tempEnemies = new Vector<EnemySprite>();
        float r2 = r*r;
        float dx,dy, d2;
        int slowest = -1;
        float sSpeed = 9999;
        float speed = 0;
        boolean found;
        EnemySprite e;
        if (this.context.enemies != null) {
            for (int i=0; i<this.context.enemies.length; i++) {
                e = this.context.enemies[i];
                if (e.validTarget() && !(exShielded && e.isShielded())) {
                    dx = e.getX() - x;
                    dy = e.getY() - y;
                    d2 = dx*dx + dy*dy;
                    if (d2 < r2) {
                        found = false;
                        for (int j=0; j<not.length; j++) {
                            if (not[j] == e) {
                                found = true;
                                break;
                            }
                        }
                        if (!found) {
                            speed = e.getSpeed();
                            if (tempEnemies.size() < n) {
                                tempEnemies.add(e);
                                if (speed < sSpeed) {
                                    sSpeed = speed;
                                    slowest = tempEnemies.size()-1;
                                }
                            } else {
                                if (speed > sSpeed) {
                                    tempEnemies.remove(slowest);
                                    tempEnemies.add(e);
                                    sSpeed = 9999;
                                    for (int s=tempEnemies.size()-1; s>=0; s--) {
                                        float tempSpeed = tempEnemies.get(s).getSpeed();
                                        if (tempSpeed < sSpeed) {
                                            sSpeed = tempSpeed;
                                            slowest = s;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        EnemySprite[] retVal = new EnemySprite[tempEnemies.size()];
        retVal = tempEnemies.toArray(retVal);
        return retVal;
    }
    
    /* Specific enemy finders
     * These can be deferred to a firecontrol tower.
     */
    public int findEnemy(int preferedEnemyNr, boolean exShielded) {
        if (this.retarget) {
            preferedEnemyNr = -1;
            this.retarget = false;
        }
        if (this.fc != null) {
            return this.fc.findEnemy(this, preferedEnemyNr, exShielded);
        }
        
        switch (this.mode) {
            case 0:
                return this.findEnemyClosest(preferedEnemyNr, exShielded);
            case 1:
                return this.findEnemyHardest(preferedEnemyNr, exShielded);
            case 2:
                return this.findEnemyWeakest(preferedEnemyNr, exShielded);
            case 3:
                return this.findEnemyFurthest(preferedEnemyNr, exShielded);
            default:
                System.err.println("TowerBase::FindEnemy: Unknown mode "+this.mode);
        }
        return -1;
    }
    
    private int findEnemyClosest(int preferedEnemyNr, boolean exShielded) {
        int foundEnemy = -1;
        float distance2Min = 1E10f;
        float distance2;
        float dx,dy;
        EnemySprite e;
        if (this.context.enemies != null) {
            if (preferedEnemyNr >= 0 && preferedEnemyNr < this.context.enemies.length) {
                e = this.context.enemies[preferedEnemyNr];
                if ( e.validTarget() && !(exShielded && e.isShielded()) ) {
                    dx = this.context.enemies[preferedEnemyNr].getX()-this.xFloat;
                    dy = this.context.enemies[preferedEnemyNr].getY()-this.yFloat;
                    distance2 = dx*dx + dy*dy;
                    if (distance2 < this.rangeCurrent2) {
                        return preferedEnemyNr;
                    }
                }
            }
            for (int i=0; i<this.context.enemies.length; i++) {
                e = this.context.enemies[i];
                if ( e.validTarget() && !(exShielded && e.isShielded()) ) {
                    dx = this.context.enemies[i].getX()-this.xFloat;
                    dy = this.context.enemies[i].getY()-this.yFloat;
                    distance2 = dx*dx + dy*dy;
                    if (distance2 < this.rangeCurrent2 && distance2 < distance2Min) {
                        distance2Min = distance2;
                        foundEnemy = i;
                    }
                }
            }
        }
        return foundEnemy;
    }
    private int findEnemyWeakest(int preferedEnemyNr, boolean exShielded) {
        
        int foundEnemy = -1;
        float distance2;
        long healthMin = 19999999999999999l;
        long health;
        float dx,dy;
        EnemySprite e;
        if (this.context.enemies != null) {
            if (preferedEnemyNr >= 0 && preferedEnemyNr < this.context.enemies.length) {
                e = this.context.enemies[preferedEnemyNr];
                if ( e.validTarget() && !(exShielded && e.isShielded()) ) {
                    dx = this.context.enemies[preferedEnemyNr].getX()-this.xFloat;
                    dy = this.context.enemies[preferedEnemyNr].getY()-this.yFloat;
                    distance2 = dx*dx + dy*dy;
                    if (distance2 < this.rangeCurrent2) {
                        return preferedEnemyNr;
                    }
                }
            }
            for (int i=0; i<this.context.enemies.length; i++) {
                e = this.context.enemies[i];
                if ( e.validTarget() && !(exShielded && e.isShielded()) ) {
                    dx = this.context.enemies[i].getX()-this.xFloat;
                    dy = this.context.enemies[i].getY()-this.yFloat;
                    distance2 = dx*dx + dy*dy;
                    health  = this.context.enemies[i].getHealth();
                    if (distance2 < this.rangeCurrent2 && health < healthMin) {
                        healthMin = health;
                        foundEnemy = i;
                    }
                }
            }
        }
        return foundEnemy;
    }
    private int findEnemyHardest(int preferedEnemyNr, boolean exShielded) {
        
        int foundEnemy = -1;
        float distance2;
        long healthMax = 0;
        long health;
        float dx,dy;
        EnemySprite e;
        if (this.context.enemies != null) {
            if (preferedEnemyNr >= 0 && preferedEnemyNr < this.context.enemies.length) {
                e = this.context.enemies[preferedEnemyNr];
                if ( e.validTarget() && !(exShielded && e.isShielded()) ) {
                    dx = this.context.enemies[preferedEnemyNr].getX()-this.xFloat;
                    dy = this.context.enemies[preferedEnemyNr].getY()-this.yFloat;
                    distance2 = dx*dx + dy*dy;
                    if (distance2 < this.rangeCurrent2) {
                        return preferedEnemyNr;
                    }
                }
            }
            for (int i=0; i<this.context.enemies.length; i++) {
                e = this.context.enemies[i];
                if ( e.validTarget() && !(exShielded && e.isShielded()) ) {
                    dx = this.context.enemies[i].getX()-this.xFloat;
                    dy = this.context.enemies[i].getY()-this.yFloat;
                    distance2 = dx*dx + dy*dy;
                    health  = this.context.enemies[i].getHealth();
                    if (distance2 < this.rangeCurrent2 && health > healthMax) {
                        healthMax = health;
                        foundEnemy = i;
                    }
                }
            }
        }
        return foundEnemy;
    }
    private int findEnemyFurthest(int preferedEnemyNr, boolean exShielded) {
        
        int foundEnemy = -1;
        float distance2;
        float progressMax = -1;
        float progress;
        float dx,dy;
        EnemySprite e;
        if (this.context.enemies != null) {
            if (preferedEnemyNr >= 0 && preferedEnemyNr < this.context.enemies.length) {
                e = this.context.enemies[preferedEnemyNr];
                if ( e.validTarget() && !(exShielded && e.isShielded()) ) {
                    dx = this.context.enemies[preferedEnemyNr].getX()-this.xFloat;
                    dy = this.context.enemies[preferedEnemyNr].getY()-this.yFloat;
                    distance2 = dx*dx + dy*dy;
                    if (distance2 < this.rangeCurrent2) {
                        return preferedEnemyNr;
                    }
                }
            }
            for (int i=0; i<this.context.enemies.length; i++) {
                e = this.context.enemies[i];
                if ( e.validTarget() && !(exShielded && e.isShielded()) ) {
                    dx = this.context.enemies[i].getX()-this.xFloat;
                    dy = this.context.enemies[i].getY()-this.yFloat;
                    distance2 = dx*dx + dy*dy;
                    progress = this.context.enemies[i].getProgress();
                    if (distance2 < this.rangeCurrent2 && progress > progressMax) {
                        progressMax = progress;
                        foundEnemy = i;
                    }
                }
            }
        }
        return foundEnemy;
    }
    
    
    @Override
    public void resetScale() {
        this.paintScale = this.context.paintScale;
        
        this.generateShapes();
        if (this.hex != null) {
            float[] realLocation = this.hex.getFloatLocation();
            this.xFloat = realLocation[0];
            this.yFloat = realLocation[1];
        } else {
            this.xFloat = 1f;
            this.yFloat = 1f;
        }
        this.xPaint = this.xFloat * this.paintScale + this.context.offsetX;
        this.yPaint = this.yFloat * this.paintScale + this.context.offsetY;
        atTranslate.setToIdentity();
        atTranslate.translate(this.xPaint, this.yPaint);
        
        this.calcDamageRange();
        this.rangeStroke = new BasicStroke(1.0f*this.context.strokeScale, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
        
        this.levelFont = new Font(Font.DIALOG, Font.PLAIN, (int)(0.40*this.paintScale));
        this.levelLocation[0] = -0.95f*this.context.hexPaintWidth;
        this.levelLocation[1] = 0.5f*this.paintScale;
        
        this.bodySVGName = this.getSvgName();
        if (this.bodySVGName != null) {
            this.bodyUsesSVG = true;
            this.bodyScale = this.context.paintScale*1.5f;
            atCorrect.setToIdentity();
            atCorrect.setToTranslation(-this.bodyScale/2, -this.bodyScale/2);
        }
        
    }
    
    @Override
    public void paint(Graphics2D g2, int gameTime) {
        Stroke defaultStroke = g2.getStroke();
        AffineTransform saveXform = g2.getTransform();
        
        g2.transform(atTranslate);
        g2.setColor(this.lineColor);
        g2.setFont(this.levelFont);
        g2.drawString(""+(this.level+1), this.levelLocation[0], this.levelLocation[1]);
        
        if (this.selected) {
            g2.setColor(this.rangeColor);
            g2.draw(this.rangeCircle);
        }
        
        if (this.bodyUsesSVG) {
            g2.transform(this.atCorrect);
            context.renderSvgImage(this.bodySVGName, g2, (int)this.bodyScale, (int)this.bodyScale);
        } else {
            for (int i=0; i<this.shapes.length; i++) {
                g2.setColor(this.shapeColor[i]);
                switch (this.shapeAction[i]) {
                    case 0:
                        g2.setStroke(this.strokes[i]);
                        g2.fill(this.shapes[i]);
                        g2.draw(this.shapes[i]);
                        break;
                    case 1:
                        g2.setStroke(this.strokes[i]);
                        g2.draw(this.shapes[i]);
                        break;
                    case 2:
                        g2.fill(this.shapes[i]);
                        break;
                }
            }
        }
        
        g2.setTransform(saveXform);
        g2.setStroke(defaultStroke);
        
    }
    
    @Override
    public Hex getHex() {
        return hex;
    }
    
    @Override
    public void setHex(Hex hex) {
        this.hex = hex;
    }
    
    @Override
    public void registerTower(Tower t) {
        if (t != this && !this.selling) {
            switch (t.getType()) {
                case firecontrol:
                    if (this.type != TowerFactory.type.upgradeDamage && this.type != TowerFactory.type.upgradeRange && this.type != TowerFactory.type.firecontrol) {
                        if (this.fc != null) {
                            this.fc.removeClient(this);
                        }
                        this.fc = (TowerFireControl)t;
                        this.fc.addClient(this);
                    }
                    break;
                case upgradeDamage:
                    if (this.type != TowerFactory.type.upgradeDamage && this.type != TowerFactory.type.upgradeRange && this.type != TowerFactory.type.firecontrol) {
                        if (!this.upgradeDamage.contains(t)) {
                            TowerUpgradeDamage tud = (TowerUpgradeDamage)t;
                            this.upgradeDamage.add(tud);
                            tud.addClient(this);
                            this.calcDamageRange();
                        }
                    }
                    break;
                case upgradeRange:
                    if (!this.upgradeRange.contains(t)) {
                        TowerUpgradeRange tur = (TowerUpgradeRange)t;
                        this.upgradeRange.add(tur);
                        tur.addClient(this);
                        this.calcDamageRange();
                    }
                    break;
            }
        }
    }
    
    @Override
    public void unregisterTower(Tower t) {
        switch (t.getType()) {
            case firecontrol:
                if (this.fc != null) {
                    this.fc.removeClient(this);
                    this.fc = null;
                }
                break;
            case upgradeDamage:
                TowerUpgradeDamage tud = (TowerUpgradeDamage)t;
                this.upgradeDamage.remove(t);
                tud.removeClient(this);
                break;
            case upgradeRange:
                TowerUpgradeRange tur = (TowerUpgradeRange)t;
                this.upgradeRange.remove(t);
                tur.removeClient(this);
                break;
        }
        this.calcDamageRange();
    }
    
    @Override
    public void doCleanup() {
        this.selling = true;
        if (this.fc != null) {
            this.fc.removeClient(this);
            this.fc = null;
        }
        int i;
        for (i=this.upgradeDamage.size()-1; i>=0; i--) {
            TowerUpgradeDamage tud = this.upgradeDamage.remove(i);
            tud.removeClient(this);
        }
        for (i=this.upgradeRange.size()-1; i>=0; i--) {
            TowerUpgradeRange tur = this.upgradeRange.remove(i);
            tur.removeClient(this);
        }
    }
    
    @Override
    public TowerFactory.type getType() {
        return type;
    }
    
    @Override
    public PanelTowerSettings getButtons() {
        return this.pts;
    }
    
    @Override
    public void addTowerChangeListener(TowerChangeListener l) {
        this.towerChangeListeners.add(l);
    }
    @Override
    public void removeTowerChangeListener(TowerChangeListener l) {
        this.towerChangeListeners.remove(l);
    }
    protected void fireTowerChangedEvent() {
        for (int i=0; i<this.towerChangeListeners.size(); i++) {
            this.towerChangeListeners.get(i).towerChanged();
        }
    }
    
}
